/**
 * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
 */
class Timer extends EventTarget {
  #delay
  #startTime
  #timeoutId
  #hasElapsedTime = 0
  static #baseId = 0
  static #timeoutIdMap = new Map()
  static #intervalIdMap = new Map()
  constructor(delay) {
    super()
    this.#delay = delay
  }
  start() {
    this.#startTime = new Date().getTime()
    const handle = () => {
      this.#hasElapsedTime += new Date().getTime() - this.#startTime
      if (this.#delay < this.#hasElapsedTime)
        this.dispatchEvent(new CustomEvent('timeout'))
      else
        this.#timeoutId = setTimeout(handle, this.#delay - this.#hasElapsedTime)
    }
    handle()
  }
  reset() {
    this.#startTime = new Date().getTime()
    this.#hasElapsedTime = 0
  }
  stop() {
    clearTimeout(this.#timeoutId)
    this.#hasElapsedTime += new Date().getTime() - this.#startTime
  }
  static setTimeout(handle, delay) {
    const timer = new Timer(delay)
    timer.addEventListener('timeout', handle)
    timer.start()
    const id = Timer.#baseId++
    Timer.#timeoutIdMap.set(id, timer)
    return id
  }
  static setInterval(handle, delay) {
    const timer = new Timer(delay)
    timer.addEventListener('timeout', () => {
      handle()
      timer.reset()
      timer.start()
    })
    timer.start()
    const id = Timer.#baseId++
    Timer.#intervalIdMap.set(id, timer)
    return id
  }
  static clearTimeout(id) {
    Timer.#timeoutIdMap.get(id).stop()
    Timer.#timeoutIdMap.delete(id)
  }
  static clearInterval(id) {
    Timer.#intervalIdMap.get(id).stop()
    Timer.#intervalIdMap.delete(id)
  }
  static wait(delay) {
    const { promise, resolve } = Promise.withResolvers()
    const timer = new Timer(delay)
    timer.addEventListener('timeout', resolve)
    timer.start()
    return promise
  }
}

/**
 * All the threads in pool are promise chains
 */
class FakeThreadPool {
  #maxThreads
  #threads = new Set()
  #taskList = []
  #pauseSign = false

  /**
   * Create a FakeThreadPool
   * @constructor
   * @param {number} maxThreads 
   */
  constructor(maxThreads) {
    if (typeof maxThreads != 'number')
      throw new TypeError('"maxThreads" is not a number')
    this.#maxThreads = maxThreads
  }

  async #createTread() {
    while (this.#taskList.length !== 0 && !this.#pauseSign) {
      const [handle, resolve, reject] = this.#taskList.shift()
      try {
        resolve(await handle())
      } catch (err) {
        reject(err)
      }
    }
  }

  async #startThread() {
    const thread = this.#createTread()
    this.#threads.add(thread)
    await thread
    this.#threads.delete(thread)
  }

  /**
   * Add a task to the task list and run later
   * @param {function} handle 
   * @returns {Promise} Resolves with the result of handle
   */
  submit(handle) {
    const { promise, resolve, reject } = Promise.withResolvers()
    this.#taskList.push([handle, resolve, reject])
    if (this.#threads.size < this.#maxThreads && !this.#pauseSign)
      this.#startThread()
    return promise
  }

  /**
   * Pause all the thread
   * @returns {FakeThreadPool}  Itself
   */
  async pause() {
    this.#pauseSign = true
    await Promise.allSettled(this.#threads)
    return this
  }

  /**
   * Create threads with min value between maxThreads and the number of tasks
   * @returns {FakeThreadPool}  Itself
   */
  async resume() {
    this.#pauseSign = false
    for (let count = this.#threads.size < this.#taskList.length ? this.#threads.size : this.#taskList.length; count > 0; count--)
      this.#startThread()
    return this
  }

  shutdown(timeout = 10000) {
    let isNotCleared = false
    const handle = () => {
      if (isNotCleared) {
        isNotCleared = true
        this.#threads.clear()
        this.#taskList = []
      }
    }
    return Promise.any([
      Timer.wait(timeout).then(handle),
      this.pause().then(handle)
    ])
  }
}

class TabSession extends EventTarget {
  static #label = 'using crawler in browser'
  static #idCache = 0
  #tab
  #id
  #heartbeatIntervalId
  constructor() { super() }
  connect(url, timeout = 10000) {
    this.#tab = window.open(url)
    if (!this.#tab) {
      this.dispatchEvent(new CustomEvent('error', { detail: new Error('opened url failed') }))
      return
    }
    window.focus()
    if (timeout >= 0)
      var connectTimeoutId = Timer.setTimeout(() => this.dispatchEvent(new CustomEvent('timeout', { detail: new Error('timeout occurred during connection') })), timeout)
    this.#id = TabSession.#idCache++
    const heartbeatHandle = () => this.#tab.postMessage({ label: TabSession.#label, id: this.#id, heartbeat: true }, '*')
    const handle = ({ data: { label, id } }) => {
      if (label !== TabSession.#label || id !== this.#id)
        return
      if (timeout >= 0) {
        Timer.clearTimeout(connectTimeoutId)
        var timer = new Timer(timeout)
        timer.addEventListener('timeout', () => this.dispatchEvent(new CustomEvent('timeout', { detail: new Error('timeout occurred while keeping the connection') })))
        timer.start()
      }
      Timer.clearInterval(this.#heartbeatIntervalId)
      window.removeEventListener('message', handle)
      this.dispatchEvent(new CustomEvent('connect'))
      window.addEventListener('message', ({ data: { label, id, heartbeat, data: detail } }) => {
        if (label != TabSession.#label || id !== this.#id)
          return
        if (timeout >= 0)
          timer.reset()
        if (heartbeat)
          return
        this.dispatchEvent(new CustomEvent('message', { detail }))
      })
      this.#heartbeatIntervalId = Timer.setInterval(heartbeatHandle, 1000)
    }
    window.addEventListener('message', handle)
    this.#heartbeatIntervalId = Timer.setInterval(heartbeatHandle, 200)
  }
  close() { }
  destory() {
    Timer.clearInterval(this.#heartbeatIntervalId)
    if (this.#tab) {
      this.#tab.close()
      this.#tab = undefined
    }
  }
  send(data) {
    this.#tab.postMessage({ label: TabSession.#label, id: this.#id, data }, '*')
  }
}

function getFromNewTab(url, handles, threadPool, timeout = 10000) {
  const session = new TabSession()
  const isFunc = typeof handles == "function"
  if (isFunc)
    handles = { value: handles }
  const promises = {}, resolves = {}, rejects = {}
  for (const key in handles) {
    const { promise, resolve, reject } = Promise.withResolvers()
    promises[key] = promise
    resolves[key] = resolve
    rejects[key] = reject
  }
  session.addEventListener('connect', () => {
    for (const key in handles)
      session.send({ key: key, handle: handles[key].toString() })
  })
  const logMap = new Map()
  session.addEventListener('message', async ({ detail: { type, error, key, data } }) => {
    if (type == 'result') {
      if (error == undefined)
        resolves[key](data)
      else
        rejects[key](error)
    } else if (type == 'log') {
      logMap.set(key, data)
      console.table([...logMap.values()])
      // if (data?.progress == 100)
      //   logMap.delete(id)
    }
  })
  session.addEventListener('timeout', ({ detail }) => {
    for (const key in rejects)
      rejects[key](detail)
  })
  session.addEventListener('error', ({ detail }) => {
    for (const key in rejects)
      rejects[key](detail)
  })
  const task = async () => {
    session.connect(url, timeout)
    await Promise.allSettled(Object.values(promises))
    session.destory()
  }
  if (threadPool)
    threadPool.submit(task)
  else
    task()
  return isFunc ? promises.value : promises
}